﻿/*!
Ext Spec !!Version!!
Copyright © Qube 2012-2014
http://extspec.codeplex.com/

This library may be used under the terms of the Apache License 2.0 (Apache).
Please see License.txt accompanying this file for more information.
!*/

module ExtSpec {
    export var global = (function () {
        return this;
    }());
}

/**
 * Core Ext Spec module.
 */
module ExtSpec {
    'use strict';

    interface IResourceConfigs {
        [key: string]: ResourceConfig;
    }

    interface IRef {
        ref: string;
        selector: string;
    }

    var resourceConfigs: IResourceConfigs,
        shortTypes: any;


    /**
     * Returns true if the given value is found in the array, otherwise false.
     * @param {Object[]} list The list of items to match.
     * @param {Object} value The item to match against the list.
     * @returns {Boolean}
     */
    function arrayMatcher(list: any[], value: any): boolean {
        if (list instanceof Array) {
            return list.indexOf(value) >= 0;
        }

        return false;
    }

    /**
     * Returns true if the given value or alias is found in the array, otherwise false.
     * @param {Object[]} list The list of items to match.
     * @param {Object} value The item or alias to match against the list.
     * @returns {Boolean}
     */
    function aliasMatcher(list: any[], value: string): boolean {
        if (list instanceof Array) {
            return list.some(
                function (item: string) {
                    return item === value || item.indexOf(value + '@') === 0;
                }
            );
        }

        return false;
    }

    /**
     * Returns true if the given name is found in the array of ref objects, otherwise false.
     * @param {Object[]} list The list of ref objects to match.
     * @param {String} value The ref name to match against the list.
     * @returns {Boolean}
     */
    function refMatcher(list: any[], value: any): boolean {
        var i: number,
            len: number,
            item: IRef;

        if (list instanceof Array) {
            for (i = 0, len = list.length; i < len; i += 1) {
                item = list[i];

                if (item && item.ref === value) {
                    return true;
                }
            }
        }

        return false;
    }

    /**
     * Returns true if the given key is found in the object, otherwise false.
     * @param {Object} object The object to match.
     * @param {String} name The key to find in the object.
     * @returns {Boolean}
     */
    function objectMatcher(object: any, key: string): boolean {
        return object && object.hasOwnProperty(key);
    }

    /**
     * Wraps the console in a mutable object.
     */
    export module ConsoleWrapper {
        var c: {
                log: (...args: any[]) => any;
                warn?: (...args: any[]) => any;
            };

        if (ExtSpec.global.console) {
            c = ExtSpec.global.console;
        } else {
            c = {
                log: function () {
                    return;
                }
            };
        }

        export function log(...args: any[]);
        export function log() {
            return c.log.apply(c, arguments);
        }

        export function warn(...args: any[]);
        export function warn() {
            var fn = c.warn || c.log;

            return fn.apply(c, arguments);
        }
    }

    /**
     * Class for resource configurations.
     */
    export class ResourceConfig {
        /**
         * Creates a new instance of the ResourceConfig class.
         * @param {String} property The resource type's property name.
         * @param {String} [suffix] The resource type's getter suffix.
         * @param {Function} [matcher] The resource type's matcher function. Defaults to valueMatcher if not defined.
         * @param {String} [itemProperty] The item property to match against if not the item itself.
         * @property {String} property The resource type's property name.
         * @property {String} suffix The resource type's getter suffix.
         * @property {Function} matcher The resource type's matcher function.
         * @property {String} itemProperty The item property to match against if not the item itself.
         */
        constructor(property: string, suffix?: string, matcher?: (list: any, value: any) => boolean, itemProperty?: string) {
            this.property = property;
            this.suffix = suffix;
            this.matcher = matcher || arrayMatcher;
            this.itemProperty = itemProperty;
        }

        public property: string;

        public suffix: string;

        public matcher: (list: any, value: any) => boolean;

        public itemProperty: string;
    }

    resourceConfigs = {
        'md': new ResourceConfig('models', 'Model', aliasMatcher),
        'vw': new ResourceConfig('views', 'View', aliasMatcher),
        'ct': new ResourceConfig('controllers', 'Controller', aliasMatcher),
        'st': new ResourceConfig('stores', 'Store', aliasMatcher),
        'rf': new ResourceConfig('refs', undefined, refMatcher, 'ref'),
        'rq': new ResourceConfig('requires'),
        'cf': new ResourceConfig('config', undefined, objectMatcher)
    };

    shortTypes = {
        'st': 'string',
        'nm': 'number',
        'nn': 'nan',
        'in': 'infinity',
        'bl': 'boolean',
        'un': 'undefined',
        'nl': 'null',
        'ob': 'object',
        'dt': 'date',
        'fn': 'function',
        'ar': 'array',
        'rg': 'regexp'
    };

    /**
     * Returns the object type of the given value as mixed case string.
     * @param {Object} value Any value of any type.
     * @returns {String}
     */
    export function typeOfObject(value: any): string {
        if (value === null) {
            return 'Null';
        }

        if (value === undefined) {
            return 'Undefined';
        }

        return Object.prototype.toString.call(value).slice(8, -1);
    }

    /**
     * Returns either the primitive or object type of a given value as a lower cased string. Includes number variants.
     * @param {Object} value Any value of any type.
     * @returns {String}
     */
    export function typeOf(value: any): string {
        if (value === null) {
            return 'null';
        }

        var type: string = typeof value;

        if (type === 'number') {
            if (isNaN(value)) {
                return 'nan';
            }

            if (!isFinite(value)) {
                return 'infinity';
            }
        }

        if (type === 'object' || type === 'function') {
            return ExtSpec.typeOfObject(value).toLowerCase();
        }

        return type;
    }

    /**
     * Returns true if the primitive or complex type of a given value matches the comparison type, otherwise false.
     * @param {Object} value Any value of any type.
     * @param {String} compare The object type to compare as a lowercase string. Also accepts the following two character abbreviations:
     *     ar: array,
     *     bl: boolean,
     *     dt: date,
     *     fn: function,
     *     in: infinity,
     *     nl: null,
     *     nm: number,
     *     nn: nan,
     *     ob: object,
     *     rg: regexp,
     *     st: string,
     *     un: undefined
     * @returns {Boolean}
     */
    export function isTypeOf(value: any, compare: string): boolean {
        var type: string = ExtSpec.typeOf(value);

        return type === (shortTypes[compare] || compare);
    }

    /**
     * Returns true if a given value is an object type and is not null, otherwise false.
     * @param {Object} value Any value of any type.
     * @returns {Boolean}
     */
    export function isObjectType(value: any): boolean {
        var type: string = typeof value;

        return value !== null && (type === 'object' || type === 'function');
    }

    /**
     * Compares two objects, returning true if all the keys in the second object have equal values in the first.
     * @param {Object} value An object to compare.
     * @param {Object} match An object containing key value pairs to match.
     * @returns {Boolean}
     */
    export function objectKeysMatch(value: any, match: any): boolean {
        var key: string;

        for (key in match) {
            if (match.hasOwnProperty(key)) {
                if (!value.hasOwnProperty(key) || match[key] !== value[key]) {
                    return false;
                }
            }
        }

        return true;
    }

    /**
     * Copies properties to the destination object from the source.
     * @param {Object} destination The destination object.
     * @param {Object} source The source object.
     * @param {String[]} [except] The properties to skip, or undefined to skip none.
     */
    export function apply(destination: any, source: any, except?: string[]): void {
        var key: string,
            skip: boolean;

        if (!ExtSpec.isObjectType(destination)) {
            throw new Error('The destination argument must be an object type.');
        }

        if (!ExtSpec.isObjectType(source)) {
            throw new Error('The source argument must be an object type.');
        }

        for (key in source) {
            if (source.hasOwnProperty(key) && (!except || except.indexOf(key) < 0)) {
                destination[key] = source[key];
            }
        }
    }

    /**
     * Ensures that each object in a namespace exists, and creates it if it does not. Returns the final object in the chain.
     * @param {String} namespace The namespace to assign using dot notation.
     * @param {Object} [root] The root object to attach the namespace. Will default to the global object if omitted.
     * @returns {Object}
     */
    export function ensureNamespace(namespace: string, root?: any): any {
        if (!ExtSpec.isTypeOf(namespace, 'st') || namespace.length < 1) {
            throw new Error('The namespace parameter must be a non empty string.');
        }

        if (root && !ExtSpec.isObjectType(root)) {
            throw new Error('The root parameter must be of an object type if supplied.');
        }

        var object: any = root || ExtSpec.global,
            keys: string[] = namespace.split('.'),
            key: string,
            i: number,
            len: number;

        for (i = 0, len = keys.length; i < len; i += 1) {
            key = keys[i];

            if (!ExtSpec.isObjectType(object[key])) {
                object[key] = Object(object[key]);
            }

            object = object[key];
        }

        return object;
    }

    /**
     * Adds a warning to the browser console.
     * @param {String} warning The warning message to output to the console.
     */
    export function warn(warning: string): void {
        warning = 'Ext Spec Warning: ' + warning;

        ConsoleWrapper.warn(warning);
    }

    /**
     * Describes the configuration argument for the obsolete method.
     */
    export interface IObsoleteConfig {
        /**
         * The method name.
         */
        method: string;
        /**
         * The namespace.
         */
        namespace?: string;
        /**
         * The message desribing alternative workarounds.
         */
        message?: string;
    }

    /**
     * Marks a method as obsolete and logs it to the console.
     * @param {String} method The method name.
     */
    export function obsolete(method: string): void;
    /**
     * Marks a method as obsolete and logs it to the console.
     * @param {Object} config The method configuration argument.
     */
    export function obsolete(config: IObsoleteConfig): void;
    /**
     * Marks a method as obsolete and logs it to the console.
     * @param {String|Object} arg The method name or configuration argument.
     */
    export function obsolete(arg: any): void {
        var fqn: string,
            message: string,
            config: IObsoleteConfig;

        if (!ExtSpec.isTypeOf(arg, 'st')) {
            config = arg;
        } else {
            config = {
                method: arg
            };
        }

        fqn = config.namespace ? config.namespace + '.' + config.method : config.method;
        message = '"' + fqn + '" is obsolete' + (config.message ? ': ' + config.message : '.');

        ConsoleWrapper.warn(message);
    }

    /**
     * Gets the static resource handling configuration for the given type.
     * @param {String} type A double character string representing a type. The following values are supported:
     *     md: model,
     *     vw: view,
     *     ct: controller,
     *     st: store,
     *     rf: ref,
     *     rq: require
     *     cf: config
     * @returns {Object}
     */
    export function getResourceConfig(type: string): ResourceConfig {
        if (!resourceConfigs.hasOwnProperty(type)) {
            throw new Error('Unsupported type "' + type + '".');
        }

        return resourceConfigs[type];
    }

    /**
     * Retrieves the configuration object from an XTemplate array.
     * @param {Object} instance The object instance in which to find the tpl.
     * @returns {Object} The configuration object.
     */
    export function getTplConfig(instance: any): any {
        if (!isObjectType(instance)) {
            throw new Error('The instance argument is undefined or not an object.');
        }

        var tpl: any = instance && instance.tpl,
            config: any;

        if (isTypeOf(tpl, 'ar')) {
            config = tpl[tpl.length - 1];

            if (isObjectType(config)) {
                return config;
            }

            throw new Error('The last element of the "tpl" array is not a configuration object.');
        }

        throw new Error('The "tpl" property must be an array.');
    }

    /**
     * Escapes a string to be used in a regular expression pattern.
     * @param {String} value The string to be escaped.
     * @returns {String} The escaped string.
     */
    export function regExpEscape(value: string): string {
        return value && value.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
    }
}
